package docbookarchivemanager.transformer;

import docbookarchivemanager.utils.Stylesheet;
import docbookarchivemanager.StartupManager;
import docbookarchivemanager.converter.Converter;
import docbookarchivemanager.utils.DomManager;
import docbookarchivemanager.utils.FileManager;
import docbookarchivemanager.utils.NamespaceContextImpl;
import docbookarchivemanager.utils.StringHelper;
import docbookarchivemanager.utils.ZipManager;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Scanner;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.apps.Fop;
import org.apache.fop.apps.FopFactory;
import org.apache.fop.apps.MimeConstants;
import org.apache.xml.resolver.CatalogManager;
import org.apache.xml.resolver.tools.CatalogResolver;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 *
 * @author Jakub Hrachovec
 */
public class Transformere {

    private static ArrayList<Stylesheet> stylesheets = new ArrayList<>();
    private static File globalOutputFolder;
    private static Boolean copyFiles = true;

    /**
     * Decides whether transformation of an XML or a DAR should be executed.
     *
     * @param fileToTransform An XML file to transform.
     * @param xsl An XSL to use.
     * @param isDar If given file is a DAR.
     * @param shouldFo If transformation should output a PDF.
     * @param params Parameters for the transformation.
     * @param outputFile Output file.
     * @throws IOException
     */
    public static void startTransformationProcess(File fileToTransform, String xsl, boolean isDar, boolean shouldFo, ArrayList<String> params, File outputFile) throws IOException, SAXException {
        if (isDar) {
            transformDocbookArchive(fileToTransform, xsl, shouldFo, params, outputFile);
        } else {
            DomManager.validateXmlFile(DomManager.XSD_FILE, fileToTransform);
            transformXML(fileToTransform, xsl, shouldFo, params, outputFile);
        }

    }

    /**
     * Prepares transformation of given Docbook Archive. Checks if it contains
     * "contents".xml and manifest.xml.
     *
     * @param file
     * @param fo
     * @param params
     * @throws IOException
     */
    private static void transformDocbookArchive(File file, String xsl, boolean fo, ArrayList<String> params, File outputFile) throws IOException, SAXException {
        getFiles();
        ZipManager.unZipIt(file, globalOutputFolder);
        if (isValid(globalOutputFolder)) {
            File manifest = new File(globalOutputFolder + File.separator + Converter.MANIFEST_NAME);
            NamespaceContextImpl.bindDefaultNamespaces();
            Document doc = DomManager.readDom(manifest);
            if (doc != null) {
                File contents = getContents(doc);
                if (contents != null) {
                    if (FileManager.isFileAnXml(contents, null)) {
                        DomManager.validateXmlFile(DomManager.XSD_FILE, contents);
                        if (xsl == null) {
                            setupStylesheets(doc);
                        }
                        prepareAndTransform(contents, xsl, fo, params, outputFile);
                    }
                } else {
                    System.out.println("ERROR: Couldn't find a contents element with path to an XML in " + Converter.MANIFEST_NAME);
                }
            } else {
                System.out.println(String.format("ERROR: Cannot read manifest.xml in %s.", file.getAbsolutePath()));
            }
        }

    }

    /**
     * Prepares transformation of given XML. Checks if it contains
     * 'xml-stylesheet' PIs.
     *
     * @param file
     * @param fo
     * @param params
     * @throws IOException
     * @throws NullPointerException
     */
    private static void transformXML(File file, String xsl, boolean fo, ArrayList<String> params, File outputFile) throws IOException {
        getFiles();
        if (xsl == null) {
            getStylesheetsFromPIs(file);
        }
        prepareAndTransform(file, xsl, fo, params, outputFile);

    }

    public static void transform(String fileToTransform, String stylesheetToUse, boolean fo, ArrayList<String> params, File outputFile) throws NullPointerException, FileNotFoundException {
        if (outputFile == null) {
            String extension;
            if (fo) {
                extension = ".pdf";
            } else {
                extension = ".html";
            }
            outputFile = new File(StartupManager.getGlobalParentFolder() + File.separator + FileManager.getFileName(new File(fileToTransform)) + extension);
        }

        try {
            System.out.println("\nTransforming ...\n");

            System.setProperty("xml.catalog.ignoreMissing", "true");
            System.setProperty("org.apache.xerces.xni.parser.XMLParserConfiguration", "org.apache.xerces.parsers.XIncludeParserConfiguration");
            System.setProperty("javax.xml.parsers.SAXParserFactory", "org.apache.xerces.jaxp.SAXParserFactoryImpl");

            // Load XML catalog resolver
            CatalogManager catalogManager = new CatalogManager();
            catalogManager.setCatalogFiles("./docbook-xsl-ns/catalog.xml");

            CatalogResolver resolver = new CatalogResolver(catalogManager);

            //If provided stylesheet is a valid URL try to resolve it.
            Source stylesheet = null;
            if (StringHelper.isValidURL(stylesheetToUse)) {
                stylesheet = resolver.resolve(stylesheetToUse, null);
            }

            if (stylesheet == null) {
                stylesheet = new StreamSource(stylesheetToUse);
            }

            TransformerFactory tFactory = TransformerFactory.newInstance();

            tFactory.setURIResolver(resolver);

            Transformer transformer
                    = tFactory.newTransformer(stylesheet);

            setParams(transformer, params);

            String baseURI = new File(fileToTransform).getParentFile().toURI().toURL().toExternalForm();

            if (fo) {
                try (OutputStream out = new BufferedOutputStream(new FileOutputStream(outputFile))) {

                    FopFactory fopFactory = FopFactory.newInstance();
                    fopFactory.setBaseURL(baseURI);
                    fopFactory.setURIResolver(resolver);
                    fopFactory.setUserConfig(new File("./conf/fop.xconf"));

                    FOUserAgent foUserAgent = fopFactory.newFOUserAgent();
                    foUserAgent.setBaseURL(fopFactory.getBaseURL());

                    Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, foUserAgent, out);

                    Result res = new SAXResult(fop.getDefaultHandler());
                    Source src = new StreamSource(fileToTransform);

                    transformer.transform(src, res);

                    System.out.println("\nTransformation Successful.");
                    System.out.println(String.format("%s has been outputted.", outputFile.getCanonicalPath()));
                } catch (SAXException ex) {
                    System.out.println("ERROR: " + ex);
                }

            } else {

                transformer.setOutputProperty(OutputKeys.INDENT, "yes");
                transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");

                //Set the parameter for location of output, mainly for chunking
                if (transformer.getParameter("base.dir") == null) {
                    transformer.setParameter("base.dir", outputFile.getParentFile());
                }

                Document doc = DomManager.readDom(new File(fileToTransform), true);
                DOMSource src = new DOMSource(doc);

                //Transform the XML and output it to given file
                transformer.transform(src, new StreamResult(outputFile));

                System.out.println("\nTransformation Successful.");
                System.out.println(String.format("%s has been outputted.\n", outputFile.getCanonicalPath()));

                //Copies referenced files in the XMLs to their new location relative to the output
                if (copyFiles) {
                    copyReferencedFiles(doc, fileToTransform, outputFile);
                }
            }

        } catch (IOException | TransformerException | NullPointerException ex) {
            System.out.println("ERROR: " + ex);
        }

    }

    private static void prepareAndTransform(File file, String xsl, boolean fo, ArrayList<String> params, File outputFile) throws IOException {
        String stylesheetToUse = xsl;

        if (stylesheetToUse == null) {
            prepareDefaultDocbookStylesheets();

            HashMap<Integer, Integer> sts = printoutStylesheets(fo);

            if (sts.isEmpty()) {
                System.out.println("ERROR: Couldn't find any stylesheets. Please use the -xsl command.");
                return;
            }

            Object o = checkForUserInput(sts);

            if (o != null) {
                if (o instanceof String) {
                    stylesheetToUse = (String) o;
                } else {
                    Stylesheet s = (Stylesheet) o;
                    if (!fo) {
                        fo = Boolean.parseBoolean(s.isFo().name().toLowerCase());
                    }
                    stylesheetToUse = s.getPath();
                }
            }
        }
        if (stylesheetToUse != null) {
            transform(file.getCanonicalPath(), stylesheetToUse, fo, params, outputFile);
        }
    }

    private static boolean isValid(File file) {
        boolean temp = true;
        File manifest = new File(file + File.separator + Converter.MANIFEST_NAME);

        if (FileManager.fileExists(manifest, false, null)) {
            if (!FileManager.sameContentType(manifest, FileManager.CONTENT_TYPES, false)) {
                temp = false;
                System.out.println("ERROR: Manifest isn't an XML file. The Dockbook Archive is corrupted.");
            }
        } else {
            temp = false;
            System.out.println("ERROR: Couldn't find manifest.xml. The Dockbook Archive is corrupted.");
        }

        return temp;

    }

    private static File getContents(Document dom) {
        NodeList nodes = DomManager.evaluateXpath(dom, "/darManifest:manifest/darManifest:contents");
        File contents = null;
        if (nodes != null) {
            Node curNode = nodes.item(0);
            String path = curNode.getAttributes().getNamedItem("path").getNodeValue().trim();
            if (path != null) {
                contents = FileManager.getAbsoluteFile(path, globalOutputFolder);
            }
        }
        return contents;
    }

    private static void setupStylesheets(Document dom) throws IOException {
        NodeList nodes = DomManager.evaluateXpath(dom, "/darManifest:manifest/darManifest:stylesheets/darManifest:stylesheet");
        if (nodes != null) {
            for (int i = 0; i < nodes.getLength(); i++) {
                Node curNode = nodes.item(i);
                //Get Stylesheet elements (contain name, path, note atts) from every styleshets element.

                NamedNodeMap atts = curNode.getAttributes();

                if (atts != null) {

                    Node path = atts.getNamedItem("path");
                    Node name = atts.getNamedItem("name");
                    Node type = atts.getNamedItem("note");
                    Node foNode = atts.getNamedItem("fo");

                    if (path != null) {
                        String pathValue = path.getNodeValue().trim();
                        String nameValue = pathValue;
                        Stylesheet.IsFormatObject isFo = Stylesheet.IsFormatObject.UNKNOWN;
                        Stylesheet.Type t = Stylesheet.Type.UNKNOWN;

                        if (name != null) {
                            nameValue = name.getNodeValue().trim();
                        }

                        if (foNode != null) {
                            String isFoString = foNode.getNodeValue().trim().toLowerCase();
                            if (Boolean.parseBoolean(isFoString)) {
                                isFo = Stylesheet.IsFormatObject.TRUE;
                            }
                            if ("false".equals(isFoString)) {
                                isFo = Stylesheet.IsFormatObject.FALSE;
                            }
                        }

                        if (type != null) {
                            for (Stylesheet.Type st : Stylesheet.Type.values()) {
                                if (type.getNodeValue().trim().equalsIgnoreCase(st.getIdent())) {
                                    t = st;
                                    break;
                                }
                            }
                        }

                        if (FileManager.isStylesheetAtUrl(pathValue)) {
                            Stylesheet newStylesheet = new Stylesheet(pathValue, isFo, nameValue, t);
                            stylesheets.add(newStylesheet);

                        } else {
                            File stylesheet = new File(globalOutputFolder + File.separator + pathValue);
                            if (FileManager.fileExists(stylesheet, false, null)) {
                                Stylesheet newStylesheet = new Stylesheet(stylesheet.getCanonicalPath(), isFo, nameValue, t);
                                stylesheets.add(newStylesheet);
                            } else {
                                System.out.println(String.format("ERROR: Couldn't find stylesheet %s in the archive.", nameValue));
                            }
                        }

                    } else {
                        System.out.println(String.format("ERROR: Path for a stylesheet entry in %s cannot be null.", Converter.MANIFEST_NAME));
                    }
                }
            }
        }

    }

    @Deprecated
    private static String getOutputExtension(File stylesheet) throws FileNotFoundException {
        Document dom = DomManager.readDom(stylesheet);
        NodeList output = DomManager.evaluateXpath(dom, "/xsl:stylesheet/xsl:output");
        if (output == null | output.getLength() == 0) {
            return ".xml";
        }
        String method = output.item(0).getAttributes().getNamedItem("method").getNodeValue().toLowerCase().trim();

        if (null != method) {
            switch (method) {
                case "xhtml":
                case "html":
                    return ".html";

                case "text":
                    return ".txt";

                default:
                    return ".xml";
            }
        } else {
            return ".xml";
        }

    }

    private static HashMap<Integer, Integer> printoutStylesheets(boolean fo) {
        System.out.println("\nSelect a stylesheet which you want to use. Enter \"exit\" to quit.\n");
//        System.out.println("0: Custom");

        HashMap<Integer, Integer> temp = new HashMap<>();

        int n = 1;
        for (int i = 0; i < stylesheets.size(); i++) {
            Stylesheet stylesheet = stylesheets.get(i);
            if (fo) {
                if (stylesheet.isFo() != Stylesheet.IsFormatObject.FALSE) {
                    System.out.print(String.format("%d: %s %s", n, stylesheet.getName(), stylesheet.getType().getNote()));
                    if (stylesheet.isFo() == Stylesheet.IsFormatObject.TRUE) {
                        System.out.print("(FO)");
                    }
                    System.out.println("");
                    temp.put(n, i);
                    n++;
                }
            } else {
                if (stylesheet.isFo() != Stylesheet.IsFormatObject.TRUE) {
                    System.out.print(String.format("%d: %s  %s\n", n, stylesheet.getName(), stylesheet.getType().getNote()));
                    temp.put(n, i);
                    n++;
                }
            }

        }
        System.out.println("");
        return temp;
    }

    private static Object checkForUserInput(HashMap<Integer, Integer> sts) throws IOException {
        int i;
        Scanner sc = new Scanner(System.in);
        do {
            while (!sc.hasNextInt()) {
                if (sc.hasNext("exit")) {
                    return null;
                }
                sc.next();
            }
            i = sc.nextInt();
        } while (i <= 0 || i > sts.size()); // We don't want to use Custom.

        if (i == 0) {
            return getCustomStylesheet();
        } else {
            return stylesheets.get(sts.get(i));
        }

    }

    @Deprecated
    private static String getCustomStylesheet() throws IOException {
        Scanner sc = new Scanner(System.in);
        String s;
        String stylesheet = null;
        boolean found = false;

        do {
            System.out.println("\nEnter Path:");
            while (!sc.hasNext()) {
            }
            s = sc.next();
            if ("exit".equals(s)) {
                return null;
            }
            if (StringHelper.isValidURL(s)) {
                if (FileManager.fileExistsAtUrl(s)) {
                    if (StringHelper.arrayContains(FileManager.CONTENT_TYPES, FileManager.getContentTypeFromUrl(s))) {
                        stylesheet = s;
                    } else {
                        System.out.println(String.format("ERROR: %s is not an XML file.", s));
                    }
                }

            } else {
                File f = FileManager.getLocalXmlFile(s, StartupManager.getStartFolder());
                if (f != null) {
                    stylesheet = f.getCanonicalPath();
                }
            }
            if (stylesheet != null) {
                found = true;
            }
        } while (!found);
        return stylesheet;
    }

    private static void getStylesheetsFromPIs(File file) throws IOException {
        Document dom = DomManager.readDom(file);
        if (dom != null) {
            String hrefRegex = "(\\s+)?href(\\s+)?=(\\s+)?\"(\\s+)?(.*?)(\\s+)?\"(\\s+)?";
            NodeList nodes = DomManager.evaluateXpath(dom, "/processing-instruction('xml-stylesheet')");
            if (nodes != null) {
                for (int i = 0; i < nodes.getLength(); i++) {
                    Node curNode = nodes.item(i);
                    String curNodeValue = curNode.getNodeValue().trim();
                    String href;

                    if (StringHelper.containsStylesheet(curNodeValue)) {
                        href = StringHelper.getValue(curNodeValue, hrefRegex, 5);
                        if (href != null) {
                            href = StringHelper.decodeURL(href.trim());
                            String xsl = null;
                            if (FileManager.isStylesheetAtUrl(href)) {
                                xsl = href;
                            } else {
                                File temp = FileManager.getLocalXmlFile(href, file);
                                if (temp != null) {
                                    xsl = temp.getCanonicalPath();
                                }
                            }
                            if (xsl != null) {
                                stylesheets.add(new Stylesheet(xsl, Stylesheet.IsFormatObject.UNKNOWN, xsl, Stylesheet.Type.PI));
                            }
                        }
                    }
                }
            }

        } else {
            throw new IOException(String.format("Cannot read %s.", file.getAbsolutePath()));
        }

    }

    private static void prepareDefaultDocbookStylesheets() {
        stylesheets.add(new Stylesheet("./docbook-xsl-ns/fo/docbook.xsl", Stylesheet.IsFormatObject.TRUE, "DocBook PDF", Stylesheet.Type.DEFAULT));
        stylesheets.add(new Stylesheet("./docbook-xsl-ns/xhtml5/docbook.xsl", Stylesheet.IsFormatObject.FALSE, "DocBook HTML 5", Stylesheet.Type.DEFAULT));
        stylesheets.add(new Stylesheet("./docbook-xsl-ns/xhtml5/chunk.xsl", Stylesheet.IsFormatObject.FALSE, "DocBook HTML 5 Chunk", Stylesheet.Type.DEFAULT));
        stylesheets.add(new Stylesheet("./docbook-xsl-ns/html/docbook.xsl", Stylesheet.IsFormatObject.FALSE, "DocBook HTML", Stylesheet.Type.DEFAULT));
        stylesheets.add(new Stylesheet("./docbook-xsl-ns/html/chunk.xsl", Stylesheet.IsFormatObject.FALSE, "DocBook HTML Chunk", Stylesheet.Type.DEFAULT));
        stylesheets.add(new Stylesheet("./docbook-xsl-ns/xhtml-1_1/docbook.xsl", Stylesheet.IsFormatObject.FALSE, "DocBook XHTML 1.1", Stylesheet.Type.DEFAULT));
        stylesheets.add(new Stylesheet("./docbook-xsl-ns/xhtml-1_1/chunk.xsl", Stylesheet.IsFormatObject.FALSE, "DocBook XHTML 1.1 Chunk", Stylesheet.Type.DEFAULT));

    }

    private static void getFiles() {
        globalOutputFolder = StartupManager.getGlobalOutputFolder();
    }

    private static void setParams(Transformer transformer, ArrayList<String> params) {
        if (params != null) {
            for (int i = 0; i < params.size(); i++) {
                transformer.setParameter(params.get(i), params.get(++i));
            }
        }
    }

    private static void copyReferencedFiles(Document doc, String fileToTransform, File outputFile) {

        //Bind DocBook Namespace
        NamespaceContextImpl.bindPrefixToNamespaceURI("db", "http://docbook.org/ns/docbook");

        //Declare for which elements referenced files will be copied (attribute is always fileref)
        String[] elements = {"imagedata", "videodata", "audiodata"};

        for (String element : elements) {
            NodeList nodes = DomManager.evaluateXpath(doc, "//db:" + element);

            if (nodes != null) {
                for (int i = 0; i < nodes.getLength(); i++) {
                    //Define baseURI of referenced files
                    String baseURI = nodes.item(i).getBaseURI();

                    NamedNodeMap atts = nodes.item(i).getAttributes();
                    if (atts != null) {
                        Node fileref = nodes.item(i).getAttributes().getNamedItem("fileref");
                        if (fileref != null) {
                            //Get value of the fileref attribute
                            String filerefValue = StringHelper.decodeURL(StringHelper.getPath(fileref.getNodeValue().trim()));
                            File reference = new File(filerefValue);
                            //If the value isn't an absolute path …
                            if (!reference.isAbsolute()) {
                                // … get XML the file it is referenced from …
                                File include = FileManager.getLocalFile(StringHelper.decodeURL(baseURI), new File(fileToTransform));
                                // … and get absolute path to the referenced file
                                reference = FileManager.getLocalFile(filerefValue, include);
                                if (reference != null) {
                                    //If the file exists then get its new location relative to the output
                                    String copyTo = FileManager.getAbsoluteFile(filerefValue, outputFile).getAbsolutePath();

                                    //and copy it there
                                    FileManager.copyFile(reference, copyTo, false);
                                }
                            }
                        }
                    }

                }
            }
        }
    }

    /**
     * @param aCopyFiles the copyFiles to set
     */
    public static void setCopyFiles(Boolean aCopyFiles) {
        copyFiles = aCopyFiles;
    }

}
